/*
 * Copyright 2013-2017 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.cloud.netflix.hystrix.dashboard;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Map;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.Header;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.PoolingClientConnectionManager;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.cloud.client.actuator.HasFeatures;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.ui.freemarker.SpringTemplateLoader;
import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer;

/**
 * @author Dave Syer
 * @author Roy Clarkson
 * @author Fahim Farook
 */
@Configuration
@EnableConfigurationProperties(HystrixDashboardProperties.class)
public class HystrixDashboardConfiguration {

	private static final String DEFAULT_TEMPLATE_LOADER_PATH = "classpath:/templates/";

	private static final String DEFAULT_CHARSET = "UTF-8";

	@Autowired
	private HystrixDashboardProperties dashboardProperties;

	@Bean
	public HasFeatures hystrixDashboardFeature() {
		return HasFeatures.namedFeature("Hystrix Dashboard", HystrixDashboardConfiguration.class);
	}

	/**
	 * Overrides Spring Boot's {@link FreeMarkerAutoConfiguration} to prefer using a
	 * {@link SpringTemplateLoader} instead of the file system. This corrects an issue
	 * where Spring Boot may use an empty 'templates' file resource to resolve templates
	 * instead of the packaged Hystrix classpath templates.
	 * @return FreeMarker configuration
	 */
	@Bean
	public FreeMarkerConfigurer freeMarkerConfigurer() {
		FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
		configurer.setTemplateLoaderPaths(DEFAULT_TEMPLATE_LOADER_PATH);
		configurer.setDefaultEncoding(DEFAULT_CHARSET);
		configurer.setPreferFileSystemAccess(false);
		return configurer;
	}

	@Bean
	public ServletRegistrationBean proxyStreamServlet() {
		final ProxyStreamServlet proxyStreamServlet = new ProxyStreamServlet();
		proxyStreamServlet.setEnableIgnoreConnectionCloseHeader(
				this.dashboardProperties.isEnableIgnoreConnectionCloseHeader());
		final ServletRegistrationBean registration = new ServletRegistrationBean(
				proxyStreamServlet, "/proxy.stream");
		registration.setInitParameters(this.dashboardProperties.getInitParameters());
		return registration;
	}

	@Bean
	public HystrixDashboardController hsytrixDashboardController() {
		return new HystrixDashboardController();
	}

	/**
	 * Proxy an EventStream request (data.stream via proxy.stream) since EventStream does
	 * not yet support CORS (https://bugs.webkit.org/show_bug.cgi?id=61862) so that a UI
	 * can request a stream from a different server.
	 */
	public static class ProxyStreamServlet extends HttpServlet {

		private static final Log log = LogFactory.getLog(ProxyStreamServlet.class);

		private static final long serialVersionUID = 1L;

		private static final String CONNECTION_CLOSE_VALUE = "close";

		private boolean enableIgnoreConnectionCloseHeader = false;

		public void setEnableIgnoreConnectionCloseHeader(
				boolean enableIgnoreConnectionCloseHeader) {
			this.enableIgnoreConnectionCloseHeader = enableIgnoreConnectionCloseHeader;
		}

		public ProxyStreamServlet() {
			super();
		}

		/**
		 * @see javax.servlet.http.HttpServlet#doGet(javax.servlet.http.HttpServletRequest
		 * request, javax.servlet.http.HttpServletResponse response)
		 */
		@Override
		protected void doGet(HttpServletRequest request, HttpServletResponse response)
				throws ServletException, IOException {
			String origin = request.getParameter("origin");
			if (origin == null) {
				response.setStatus(500);
				response.getWriter()
						.println(
								"Required parameter 'origin' missing. Example: 107.20.175.135:7001");
				return;
			}
			origin = origin.trim();

			HttpGet httpget = null;
			InputStream is = null;
			boolean hasFirstParameter = false;
			StringBuilder url = new StringBuilder();
			if (!origin.startsWith("http")) {
				url.append("http://");
			}
			url.append(origin);
			if (origin.contains("?")) {
				hasFirstParameter = true;
			}
			Map<String, String[]> params = request.getParameterMap();
			for (String key : params.keySet()) {
				if (!key.equals("origin")) {
					String[] values = params.get(key);
					String value = values[0].trim();
					if (hasFirstParameter) {
						url.append("&");
					}
					else {
						url.append("?");
						hasFirstParameter = true;
					}
					url.append(key).append("=").append(value);
				}
			}
			String proxyUrl = url.toString();
			log.info("\n\nProxy opening connection to: " + proxyUrl + "\n\n");
			try {
				httpget = new HttpGet(proxyUrl);
				HttpClient client = ProxyConnectionManager.httpClient;
				HttpResponse httpResponse = client.execute(httpget);
				int statusCode = httpResponse.getStatusLine().getStatusCode();
				if (statusCode == HttpStatus.SC_OK) {
					// writeTo swallows exceptions and never quits even if outputstream is
					// throwing IOExceptions (such as broken pipe) ... since the
					// inputstream is infinite
					// httpResponse.getEntity().writeTo(new
					// OutputStreamWrapper(response.getOutputStream()));
					// so I copy it manually ...
					is = httpResponse.getEntity().getContent();

					// set headers
					copyHeadersToServletResponse(httpResponse.getAllHeaders(), response);

					// copy data from source to response
					OutputStream os = response.getOutputStream();
					int b = -1;
					while ((b = is.read()) != -1) {
						try {
							os.write(b);
							if (b == 10 /** flush buffer on line feed */
							) {
								os.flush();
							}
						}
						catch (Exception ex) {
							if (ex.getClass().getSimpleName()
									.equalsIgnoreCase("ClientAbortException")) {
								// don't throw an exception as this means the user closed
								// the connection
								log.debug("Connection closed by client. Will stop proxying ...");
								// break out of the while loop
								break;
							}
							else {
								// received unknown error while writing so throw an
								// exception
								throw new RuntimeException(ex);
							}
						}
					}
				}
				else {
					log.warn("Failed opening connection to " + proxyUrl + " : "
							+ statusCode + " : " + httpResponse.getStatusLine());
				}
			}
			catch (Exception ex) {
				log.error("Error proxying request: " + url, ex);
			}
			finally {
				if (httpget != null) {
					try {
						httpget.abort();
					}
					catch (Exception ex) {
						log.error("failed aborting proxy connection.", ex);
					}
				}

				// httpget.abort() MUST be called first otherwise is.close() hangs
				// (because data is still streaming?)
				if (is != null) {
					// this should already be closed by httpget.abort() above
					try {
						is.close();
					}
					catch (Exception ex) {
						// ignore errors on close
					}
				}
			}

		}

		private void copyHeadersToServletResponse(Header[] headers,
				HttpServletResponse response) {
			for (Header header : headers) {
				// Some versions of Cloud Foundry (HAProxy) are
				// incorrectly setting a "Connection: close" header
				// causing the Hystrix dashboard to close the connection
				// to the stream
				// https://github.com/cloudfoundry/gorouter/issues/71
				if (this.enableIgnoreConnectionCloseHeader
						&& HttpHeaders.CONNECTION.equalsIgnoreCase(header.getName())
						&& CONNECTION_CLOSE_VALUE.equalsIgnoreCase(header.getValue())) {
					log.warn("Ignoring 'Connection: close' header from stream response");
				}
				else if (!HttpHeaders.TRANSFER_ENCODING.equalsIgnoreCase(header.getName())) {
					response.addHeader(header.getName(), header.getValue());
				}
			}
		}

		@SuppressWarnings("deprecation")
		private static class ProxyConnectionManager {

			private final static PoolingClientConnectionManager threadSafeConnectionManager = new PoolingClientConnectionManager();

			private final static HttpClient httpClient = new DefaultHttpClient(
					threadSafeConnectionManager);

			static {
				log.debug("Initialize ProxyConnectionManager");
				/* common settings */
				HttpParams httpParams = httpClient.getParams();
				HttpConnectionParams.setConnectionTimeout(httpParams, 5000);
				HttpConnectionParams.setSoTimeout(httpParams, 10000);

				/* number of connections to allow */
				threadSafeConnectionManager.setDefaultMaxPerRoute(400);
				threadSafeConnectionManager.setMaxTotal(400);
			}

		}

	}
}
